C++20 类与接口编程初窥 concept|module

XiLaiTL大约 7 分钟

C++20 类与接口编程初窥 concept|module

使用模块写类的正确操作

模块文件的架构:

C++ 中的命名模块教程 | Microsoft Learnopen in new window

模块 (C++20 起) - cppreference.comopen in new window

模块接口单元与模块实现单元

模块接口单元

myModule.ixx

module; //标志着全局模块的起点
//=============全局模块片段,在这以下导入头文件=================//
//用于导入旧版本的头文件,这样子不会被当作模块的一部分
#include<xxxx> 


export module myModule; // 标志着我们的模块的起点,模块名为myModule。模块名可以带.,比如myModule.abc
//=============本模块导入区,导入其他模块=================//
import <iostream>;
import xxxxxx;
using std::cout,std::operator<<,std::endl;

//=============本模块导出区,导出接口===================//
export{
	//在大括号里的内容都会被导出
}
export auto test()->void; //导出函数的声明
namespace myNamespace{
	export auto test1()->void;
}



module :private; // 标志着本模块私有的起点,只对本文件可见,对其他从属于本模块的文件也不可见
//=============本模块私有区,用于实现函数=================//
auto test()->void{
	cout<<"test"<<endl;
}

auto myNamespace::test1()->void{
	cout<<"test1"<<endl;
}

模块实现单元

myModuleImpl.cpp

module myModule;
//=============本模块实现区,用于实现函数=================//

模块分区

主模块接口

myModule.ixx

export myModule;
export import :part1;
export import :part2;

模块部分接口1

myModulePart1.ixx

export myModule:part1;

模块部分接口2

myModulePart2.ixx

export myModule:part2;

用模块编写类

直接说结论

导出的类不仅仅要声明(仅有类名),而且必须经过定义(还有类的成员变量和成员方法)。

类的方法的实现可以直接在导出部分中,也可以在module:private中,也可以在实现文件.cpp中。

在以往我们使用的是源文件和头文件分离的方式定义类,在.h文件中写类的声明,在.cpp文件中写类的实现,在其他文件中使用#include头文件的方式进行。

现在如果要用实现与定义的分离,则可以按结论所说的,分离在同一个文件,实现部分写在module:private部分下;分离在不同的文件下,定义部分在.ixx文件,实现部分在.cpp部分

image-20221114170949745
image-20221114170949745
image-20221114171146762
image-20221114171146762

以下是编译失败的案例

image-20221114165606412
image-20221114165606412
image-20221114165719619
image-20221114165719619
image-20221114170445582
image-20221114170445582

相互引用的类的处理

最好声明和定义在同一个模块文件下,不然会出现循环引用模块的错误。

myModule.ixx

export module myModule;
class A;
export class B{
	A* a;
};
export class A{
	B b;
};

模块的其他踩坑点

微软MSVC在C++23引入了std.core,但是在此之前,我们使用C++20时,导入标准头文件要按顺序导入,不然会出现使用不完整类型的错误。

比如:

import <sstream>;
import <iostream>;
import <string>;

如果不按照这个顺序的话,使用stringstream会出现错误,但是编译会成功。

constexpr函数只能写在export下,不能定义和声明分开。

对于使用function<void()>的默认参数为[](){} 的函数,无法使用。需要先定义一个defaultFun=[](){}

其他不符合导出要求的内容不能被导出:

一文读懂C++20 新特性之module(模块)open in new window

模块 (C++20 起) - cppreference.comopen in new window

concept实现接口

c++ - Concept that requires a certain return type of member - Stack Overflowopen in new window

其他资料:

C++20: Concept详解以及个人理解 - 知乎 (zhihu.com)open in new window

C++20详解:Concept - 知乎 (zhihu.com)open in new window

一个强迫症友好的C++20 concept的写法 - 知乎 (zhihu.com)open in new window

使用concept可以实现描述一个对象的成员属性类型和成员函数类型。

导入部分

export module TestConcept.Class;
import <concepts>;
import <iostream>;
import <string>;
using std::string;
using std::same_as;
using std::common_with;
using std::cout, std::operator&, std::endl;

对于我们的测试类,我们相当于定义了一个name:string、score:int的get set方法。现在我们想要将它抽象为一个接口,用来识别所有满足<拥有public name、score以及其get set方法>的条件的对象。

export class TestClass {
public:
	string name;
	int score;
	auto getName() -> string { return name; }
	auto setName(string _name) -> void { name = _name; }
	auto getScore() -> int { return score; }
	auto setScore(int _score) -> void { score = _score; }
	auto moreFun() -> void {}

};

茴字的四种写法

template<typename ClassName>
concept InterfaceName = requires(ClassName object) {
	requires same_as<decltype(object.name), string>;
	requires same_as<decltype(object.score), int>;
	requires requires(string s) { {object.setName(s)}->same_as<void>; };
	requires requires() { {object.getName() }->same_as<string>; };
	requires requires(int i) { {object.setScore(i)}->same_as<void>; };
	requires requires() { {object.getScore()}->same_as<int>; };
};

template<typename ClassName>
concept InterfaceName1 = requires(ClassName object) {
	{object.name }->same_as<string&>; //如果用common_with则不用加&
	{object.setName(string{})}->same_as<void>;
	{object.getName() }->same_as<string>;

	{object.score }->same_as<int&>;
	{object.setScore(int{})}->same_as<void>;
	{object.getScore() }->same_as<int>;

};

template<typename ClassName>
concept InterfaceName2 = requires(ClassName object) {
	requires requires(string name) {
		{object.name}->common_with<string>;
		{object.setName(name)}->same_as<void>;
		{object.getName() }->same_as<string>;
	};
	requires requires(int score) {
		{object.score}->common_with<int>;
		{object.setScore(score)}->same_as<void>;
		{object.getScore() }->same_as<int>;
	};
};

测试方法

export auto TestClassTest1(InterfaceName1 auto x) {
	x.setName("123");
	cout << x.getName() << endl;
	x.setScore(123);
	cout << x.getScore() << endl;
}

回过头来讲用到的concept语法

concept语法分为两个部分:concept的定义与concept的使用

concept的定义

这里只讲我们用到的部分

template<typename ClassName>
concept InterfaceName2 = requires(ClassName object) {

};

这样就定义好了concept语句了。

requires语句可以看作一个可以自定义入参的函数,接收参数值,在大括号内处理动作。

这个动作就是约束条件。concept的约束条件有四种:

简单约束(Simple Requirements)
expression;

单纯检查它是否合法

类型约束(Type Requirements)
typename XXX;

XXX类型在concept进行求值时是存在的

复合约束(Compound Requirements)
{expression} noexcept -> concept2;

表达式带入成为concept2<delctype(expression)>,判断是否满足。

如果用了noexcept,表达式必须不能可能抛出异常

嵌套约束(Nested Requirements)
requires concept3;

直接判断是否满足concept3。

由此,我们可以写出不带名字的concept3:requires(){}

完整的为

template<typename ClassName>
concept InterfaceName = requires(ClassName object) {
	requires requires(){}
}

用了三个requires。

concept的使用

这里只将我们用到的部分,其他内容请见前面的资料

作为concept内的约束部分

见上用到concept都可以。

使用在函数/类里

这里只说使用在函数的一种情况

auto test(Concept3 auto x)->void{

}

就是说参数x满足Concept3了。

完整解释

template<typename ClassName>
concept InterfaceName = requires(ClassName object) {
	requires same_as<decltype(object.name), string>;
	requires same_as<decltype(object.score), int>;
	requires requires(string s) { {object.setName(s)}->same_as<void>; };
	requires requires() { {object.getName() }->same_as<string>; };
	requires requires(int i) { {object.setScore(i)}->same_as<void>; };
	requires requires() { {object.getScore()}->same_as<int>; };
};

object.xxx的意思是ClassName的类型拥有xxx的成员属性,

同理object.xxxopen in new window()的意思是它拥有xxx的成员方法。

我们用嵌套约束引入了参数用于定义object.xxxopen in new window(i)的参数i。

我们用复合约束的类型约束条件来声明返回值应该有的类型。

same_as本来接收两个类型参数,用来判断两个类型是否同一个,在这里,其中一个参数来自表达式delc,另一个在我们定义的约束条件中。以此,我们可以定义函数的返回值类型。

template<typename ClassName>
concept InterfaceName1 = requires(ClassName object) {
	{object.name }->same_as<string&>; //如果用common_with则不用加&
	{object.setName(string{})}->same_as<void>;
	{object.getName() }->same_as<string>;
	{object.score }->same_as<int&>;
	{object.setScore(int{})}->same_as<void>;
	{object.getScore() }->same_as<int>;
};

这里我们则用了复合约束的方式,直接干净地声明。在函数参数的声明中,我们直接利用了默认初始化的语法当作参数。

注意看,成员属性的类型应该为引用!如果对它进行操作比如强制类型转换{static_cast<int>(object.score) }->same_as<int>;、运算(+0)等等就不用了,但是很显然这样做是不符合我们的要求的。或者把same_as换成common_with,就可以不用使用&了,当然语义发生变化了。

export auto TestClassTest1(InterfaceName1 auto x) {
	x.setName("123");
	cout << x.getName() << endl;
	x.setScore(123);
	cout << x.getScore() << endl;
}

由于我们在InterfaceName1中定义了它有get、set方法,所以在函数中可以直接使用,而不会报错。

这样子,我们使用了concept实现了类的接口的定义。

确定语义

在之前使用虚类作为接口时,可以通过继承确定本类中的方法究竟是实现了哪个接口,因此可以区分例如:void kill(int num);和void kill(int signal);的语义。在使用concept时,只考虑鸭子类型,只考虑函数签名是否一致,不考虑语义。

为了区分语义,我们可以通过增添一个特殊的成员变量或者增添一个嵌套子类型来区分不同的concept。

例如,在concept中添加:

typename ClassName::InterfaceCommonName;

在测试类中添加:

public:
	using InterfaceCommonName = int;

以此来确定本类与接口的联系。

完整代码:

template<typename ClassName>
concept InterfaceName1 = requires(ClassName object) {
	typename ClassName::InterfaceCommonName;
	{object.name }->same_as<string&>;
	{object.setName(string{})}->same_as<void>;
	{object.getName() }->same_as<string>;
};
export class TestClass {
public:
	using InterfaceCommonName = int;
	string name;
	auto getName() -> string { return name; }
	auto setName(string _name) -> void { name = _name; }
	auto moreFun() -> void {}

};
上次编辑于:
贡献者: XiLaiTL